package com.hqd.utils.pack.complier;

import com.hqd.utils.file.SimpleFileUtils;
import com.hqd.utils.pack.bean.ProjectInfo;
import com.hqd.utils.pack.loader.LibClassLoader;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.core.compiler.IProblem;
import org.eclipse.jdt.internal.compiler.Compiler;
import org.eclipse.jdt.internal.compiler.*;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader;
import org.eclipse.jdt.internal.compiler.classfmt.ClassFormatException;
import org.eclipse.jdt.internal.compiler.env.ICompilationUnit;
import org.eclipse.jdt.internal.compiler.env.INameEnvironment;
import org.eclipse.jdt.internal.compiler.env.NameEnvironmentAnswer;
import org.eclipse.jdt.internal.compiler.impl.CompilerOptions;
import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;

/**
 * 编译修改的文件
 */
public class HotCompiler extends AbstractCompiler {
    private List<File> files;

    public HotCompiler(ProjectInfo projectInfo, List<File> files) {
        super(projectInfo);
        this.files = files;
    }

    @Override
    protected void doCompile() throws Exception {
        /**
         * 控制错误信息的locale、格式等。这里可直接使用new DefaultProblemFactory(Locale.ENGLISH)，即返回英文的错误信息
         */
        IProblemFactory problemFactory = new DefaultProblemFactory(Locale.ENGLISH);
        /**
         * 遇到错误时怎么办。一般采用DefaultErrorHandlingPolicies.exitOnFirstError()，即遇到第一个错误就退出
         */
        IErrorHandlingPolicy policy = DefaultErrorHandlingPolicies.exitOnFirstError();

        /**
         * The JDT compiler
         */
        Compiler jdtCompiler = new Compiler(
                new NameEnvironment(), policy, getCompilerOptions(), new CompilerRequestor(), problemFactory);
        String testResourcePath = new File(projectPath, projectInfo.getTestResourcePath()).getAbsolutePath();
        String srcResourcePath = new File(projectPath, projectInfo.getResourcePath()).getAbsolutePath();
        List<ICompilationUnit> javaFiles = new ArrayList<>();
        for (File file : files) {
            if (StringUtils.equalsIgnoreCase(SimpleFileUtils.getFileSuffix(file), FILE_SUFFIX_JAVA)) {
                if (file.getAbsolutePath().startsWith(testSrcPath.getAbsolutePath()) && projectInfo.isSkipTest()) {
                    continue;
                }
                javaFiles.add(new CompilationUnit(file));
            } else {
                File path = new File(file.getAbsolutePath().replace(testResourcePath, outPath.getAbsolutePath())
                        .replace(srcResourcePath, outPath.getAbsolutePath())).getParentFile();
                addResources(path, file);
            }
        }
        jdtCompiler.compile(javaFiles.toArray(new ICompilationUnit[]{}));
    }

    private class CompilerRequestor implements ICompilerRequestor {
        public void acceptResult(CompilationResult result) {
            // If error
            if (result.hasErrors()) {
                for (IProblem problem : result.getErrors()) {
                    String className = new String(problem.getOriginatingFileName()).replace("/", ".");
                    className = className.substring(0, className.length() - 5);
                    String message = problem.getMessage();
                    if (problem.getID() == IProblem.CannotImportPackage) {
                        // Non sense !
                        message = problem.getArguments()[0] + " cannot be resolved";
                    }
                    throw new RuntimeException(className + ":" + message);
                }
            }

            // Something has been compiled
            ClassFile[] clazzFiles = result.getClassFiles();
            for (int i = 0; i < clazzFiles.length; i++) {
                String clazzName = join(clazzFiles[i].getCompoundName());

                // save to disk as .class file
                File target = new File(outPath, clazzName.replace(".", "/") + FILE_SUFFIX_CLASS);
                try {
                    FileUtils.writeByteArrayToFile(target, clazzFiles[i].getBytes());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    /**
     * 用于根据JDT传过来的包名类名，找到正确的字节码或者源文件给它
     */
    private class NameEnvironment implements INameEnvironment {
        private ClassLoader libClassLoader;

        private NameEnvironment() {
            libClassLoader = LibClassLoader.newLibClassLoader(projectInfo.getLibPath());
        }

        /**
         * @param compoundTypeName {{'j','a','v','a'}, {'l','a','n','g'}}
         */
        public NameEnvironmentAnswer findType(final char[][] compoundTypeName) {
            return findType(join(compoundTypeName));
        }

        public NameEnvironmentAnswer findType(final char[] typeName, final char[][] packageName) {
            return findType(join(packageName) + "." + new String(typeName));
        }

        /**
         * @param name like `aaa`,`aaa.BBB`,`java.lang`,`java.lang.String`
         */
        private NameEnvironmentAnswer findType(final String name) {
            logger.info("### to find the type: " + name);

            // check data dir first
            File file = new File(srcPath, name.replace('.', '/') + FILE_SUFFIX_JAVA);
            if (file.isFile()) {
                return new NameEnvironmentAnswer(new CompilationUnit(file), null);
            }
            String classPath = name.replace(".", "/") + FILE_SUFFIX_CLASS;
            // 去系统路径找
            try {
                InputStream input = this.getClass().getClassLoader().getResourceAsStream(classPath);
                /**
                 * 找不到，则去依赖查找
                 */
                if (input == null && libClassLoader != null) {
                    input = libClassLoader.getResourceAsStream(classPath);
                }
                if (input != null) {
                    byte[] bytes = IOUtils.toByteArray(input);
                    if (bytes != null) {
                        ClassFileReader classFileReader = new ClassFileReader(bytes, name.toCharArray(), true);
                        return new NameEnvironmentAnswer(classFileReader, null);
                    }
                }
            } catch (ClassFormatException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }

            logger.warn("### type not found: " + name);
            return null;
        }

        public boolean isPackage(char[][] parentPackageName, char[] packageName) {
            String name = new String(packageName);
            if (parentPackageName != null) {
                name = join(parentPackageName) + "." + name;
            }

            File target = new File(srcPath, name.replace('.', '/'));

            // only return false if it's a file
            // return true
            // even if it doesn't exist
            return !target.isFile();
        }

        public void cleanup() {
        }
    }

    private class CompilationUnit implements ICompilationUnit {

        private File file;

        public CompilationUnit(File file) {
            this.file = file;
        }

        @Override
        public char[] getContents() {
            try {
                return FileUtils.readFileToString(file).toCharArray();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public char[] getMainTypeName() {
            return file.getName().replace(FILE_SUFFIX_JAVA, "").toCharArray();
        }

        @Override
        public char[][] getPackageName() {
            String fullPkgName = this.file.getParentFile().getAbsolutePath()
                    .replace(srcPath.getAbsolutePath(), "").replace(testSrcPath.getAbsolutePath(), "");

            fullPkgName = fullPkgName.replace("/", ".").replace("\\", ".");
            fullPkgName = fullPkgName.substring(1);

            String[] items = fullPkgName.split("[.]");
            char[][] pkgName = new char[items.length][];
            for (int i = 0; i < items.length; i++) {
                pkgName[i] = items[i].toCharArray();
            }
            return pkgName;
        }

        @Override
        public boolean ignoreOptionalProblems() {
            return false;
        }

        @Override
        public char[] getFileName() {
            return this.file.getName().toCharArray();
        }

    }

    private String join(char[][] chars) {
        StringBuilder sb = new StringBuilder();
        for (char[] item : chars) {
            if (sb.length() > 0) {
                sb.append(".");
            }
            sb.append(item);
        }
        return sb.toString();
    }

    /**
     * 取回编译结果。如果编译成功，则可以拿到编译后的字节码二进制数据，否则可以拿到错误信息
     */
    private CompilerOptions getCompilerOptions() {
        Map settings = new HashMap();
        settings.put(CompilerOptions.OPTION_ReportMissingSerialVersion, CompilerOptions.IGNORE);
        settings.put(CompilerOptions.OPTION_LineNumberAttribute, isContainsDebugLevel("line") ? CompilerOptions.GENERATE : CompilerOptions.DO_NOT_GENERATE);
        settings.put(CompilerOptions.OPTION_SourceFileAttribute, isContainsDebugLevel("source") ? CompilerOptions.GENERATE : CompilerOptions.DO_NOT_GENERATE);
        settings.put(CompilerOptions.OPTION_ReportDeprecation, CompilerOptions.IGNORE);
        settings.put(CompilerOptions.OPTION_ReportUnusedImport, CompilerOptions.IGNORE);
        settings.put(CompilerOptions.OPTION_Encoding, projectInfo.getEncoding());
        settings.put(CompilerOptions.OPTION_LocalVariableAttribute, isContainsDebugLevel("vars") ? CompilerOptions.GENERATE : CompilerOptions.DO_NOT_GENERATE);
        settings.put(CompilerOptions.OPTION_PreserveUnusedLocal, CompilerOptions.PRESERVE);
        settings.put(CompilerOptions.OPTION_Source, projectInfo.getJdkVersion());
        settings.put(CompilerOptions.OPTION_TargetPlatform, projectInfo.getJdkVersion());
        settings.put(CompilerOptions.OPTION_Compliance, projectInfo.getJdkVersion());
        return new CompilerOptions(settings);
    }

    public List<File> getFiles() {
        return files;
    }

    public void setFiles(List<File> files) {
        this.files = files;
    }
}
